Skip to content

perf(bake): kill the per-tile recompute that dominated Inland-ENC bakes#21

Merged
beetlebugorg merged 2 commits into
mainfrom
perf/inland-bake
Jul 13, 2026
Merged

perf(bake): kill the per-tile recompute that dominated Inland-ENC bakes#21
beetlebugorg merged 2 commits into
mainfrom
perf/inland-bake

Conversation

@beetlebugorg

Copy link
Copy Markdown
Owner

Inland-ENC cells were minutes to bake because two per-feature computations ran
live on every tile a feature spans instead of once per cell. Both are moved into
the baker's existing per-cell cache pass. Output is byte-identical — verified
by matching sha256 on German IENC cells before/after; each cache is a strict
superset of what the per-tile path consults, so a cached value is at worst spurious,
never wrong.

1. Cache the drawn boundary's world coords per cell

An area/line whose stroked boundary is the masked subset of its geometry
(needsDrawableBoundary — MASK/USAG edge flags, or a coast-coincident edge) restroked
only drawableLineParts. That subset is tile-invariant, but it was reconstructed and
reprojected with the transcendental web-mercator projection
(tan/log/cos) on
every tile — while the fill geometry has ridden the geo_world world-coord cache for a
while. On river cells with long shared coast boundaries this was the last per-tile
projection hotspot: 1R7EMS02 ran ~1.3 billion projections (~41k/tile).

Now buildDrawnBoundary assembles the drawn parts once per cell and precomputes each
vertex's lonLatToWorld, so the per-tile stroke reprojects with a linear worldToTile.
Cache guard mirrors the emit-side guard exactly (any prim, not areas only — a masked
LINE takes the same path and was the bulk of the cost).

1R7EMS02: 127s → 48s.

2. Cache the '?' (QUESMRK1) fallback label point

Unmapped S-57 areas (no S-101 class) take the QUESMRK1 ? fallback, anchored at the
polygon's pole-of-inaccessibility (areaRepresentativePoint). They carry a null/errored
portrayal stream, so buildLabelCache skipped them and the polylabel search — quadratic
in vertex count — ran live for every tile the polygon spanned. IENC cells are full of
these (objl 17000+ classes with no mapping, over huge river/fairway polygons).

buildLabelCache now also precomputes the representative point for the QUESMRK1 fallback
(and the stream-independent INFORM01 marker), so the search runs once per cell.

1R8BR858: 31.4s → 1.9s. 1W7D2250: 110s → 2.9s.

Notes

  • No behaviour change: project == worldToTile ∘ lonLatToWorld, and a null cache slot
    keeps the feature on the live reconstruct-and-project path.
  • Touches chart.zig (wire the cache in the bake pass), s57.zig (DrawnBoundary +
    Cell.drawn_boundary), scene.zig (buildDrawnBoundary, extended buildLabelCache,
    cached stroke path).

Unmapped S-57 area features (no S-101 class mapping) take the QUESMRK1
'?' fallback in the per-tile emit, which anchors the marker at the area's
pole-of-inaccessibility (areaRepresentativePoint). Those features carry a
null/errored portrayal stream, so buildLabelCache skipped them and the
polylabel search ran LIVE for every tile the polygon spanned — quadratic
in the vertex count and repeated across hundreds of tiles.

Inland-ENC cells are full of such areas (objl 17000+ IENC classes with no
mapping, over huge river/fairway polygons), which made them minutes to bake.

Extend buildLabelCache to also precompute the representative point for the
QUESMRK1 fallback (and the stream-independent INFORM01 marker), mirroring
the emit-side guards exactly so the cached set stays a strict superset of
what the per-tile path consults. Output is byte-identical (verified: same
sha256 on a German IENC cell before/after); the search now runs once per
cell instead of once per tile. 1R8BR858 31.4s->1.9s, 1W7D2250 110s->2.9s.
An area/line feature whose stroked boundary differs from its full geometry
(needsDrawableBoundary: MASK/USAG edge flags, or a coast-coincident edge on a
non-coast-definer area) restrokes only the drawableLineParts SUBSET. That
subset was reconstructed AND reprojected with the transcendental web-mercator
projection (tan/log/cos) on EVERY tile the feature spans — while the fill
geometry has ridden the tile-invariant geo_world world-coord cache for a while.

On Inland-ENC river cells (fairways/depth areas with long shared coast
boundaries, DEPCNT contours, masked meta boundaries) this dominated the bake:
1R7EMS02 ran ~1.3 billion transcendental projections, ~41k per tile.

Cache the drawn boundary once per cell (DrawnBoundary: the drawableLineParts
geometry + each vertex's lonLatToWorld), so the per-tile stroke reprojects with
a linear worldToTile. The cache condition mirrors the emit-side guard exactly —
needsDrawableBoundary, ANY prim (a masked LINE takes the same path and was the
bulk of the cost), not areas only. Byte-identical (project == worldToTile ∘
lonLatToWorld; verified same sha256 on 6 German IENC cells). 1R7EMS02 127s->48s.
@beetlebugorg beetlebugorg merged commit b2ba066 into main Jul 13, 2026
5 checks passed
@beetlebugorg beetlebugorg deleted the perf/inland-bake branch July 13, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant